package easypal.brilliard.world.brilliards.communication.impl;


import easypal.brilliard.util.TraceUtil;
import easypal.brilliard.world.brilliards.communication.CommunicationAgent;
import easypal.brilliard.world.brilliards.communication.CommunicationChannel;
import easypal.brilliard.world.brilliards.communication.CommunicationException;
import easypal.brilliard.world.brilliards.communication.CommunicationListener;


public class CommunicationServer implements Runnable {
	protected int port = 60000;
	
	protected BrilliardsKernel kernel;
	
	protected CommunicationServerState state = CommunicationServerState.invalid;
	
	protected final static String connect_none = "connect_none";
	protected final static String connect_one = "connect_one";
	protected final static String connect_two = "connect_two";
	protected final static String connect_invalid = "connect_invalid";
	protected String connectStatus = connect_invalid;
	
	protected CommunicationListener listener;
	
	protected CommunicationReceiverServerImpl receiver;
	
	protected Thread threadOne;
	protected Thread threadTwo;
	protected CommunicationAgent agentOne;
	protected CommunicationAgent agentTwo;
	
	protected boolean isDestroyed;
	

	public CommunicationServerState getState() {
		return state;
	}
	public void setState(CommunicationServerState state) {
		this.state = state;
	}
	/*
	 * initialize the runnable server
	 */
	public void init() throws CommunicationException {

		/* Every TCP server have to have a listener */
		listener = new CommunicationListenerSocketImpl(this.port);
		listener.init();
	
		/* Receiver is a delegate, I think. */
		receiver = new CommunicationReceiverServerImpl(this);
		
		/* Server connection state. State machine is a so important concept which I like. */
		this.connectStatus = connect_none;
		
		/* Server's state */
		this.setState(CommunicationServerState.wait_first);
		
		/* a invaluable flag, for now*/
		isDestroyed = false;
	}
	public void destroy(){
		isDestroyed = true;
		this.listener.destroy();
		
		this.connectStatus = connect_invalid;
		this.setState(CommunicationServerState.invalid);
		this.offline();
	}
	@Override
	public void run() {
		long delay = 1000;
		CommunicationChannel channel = null;
		CommunicationAgent agent = null;
		
		TraceUtil.trace(this, "Ready to accept connection.");
		while (!isDestroyed) {
			/* if the server is not waiting agents, the listener don't has to 
			 * accept the agent, and destroy it. I'm lazy and I have no task now,
			 * so let me sleep a while. */
			if (this.connectStatus == connect_two) {
				
					try {
						Thread.sleep(delay);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} finally {

					}
					
					continue;
			}
			
			/*
			 * It's time to work: accept agents.
			 */
			channel = null;
			agent = null;
			try {
				channel = listener.accept();
				channel.init();
				
			} catch (CommunicationException ex) {
				/* Unfortunately, server can't open the communication channel. 
				 * Destroy it then continue to accept the next channel. */
				if (channel != null) {
					channel.destroy();
				}
				continue;
			}
			
			/* Well, a agent is connected. */
			agent = new CommunicationAgentImpl(channel, receiver);
			
			TraceUtil.trace(this, "Accept a connection agent");
			
			if (this.connectStatus == connect_none) {
				// if this agent is the first player.
				receiver.setAgentOne(agent);
				agentOne = agent;
				
				// create a thread to service this player
				try {
					agent.init();
				} catch (CommunicationException e) {
					/* This is a corrupted agent, destroy it as before. Then to serve
					 * the next agent. */
					agent.destory();
					continue;
				}
				threadOne = new Thread(agent);
				threadOne.setUncaughtExceptionHandler(this.new AgentThreadUncaughtExceptionHandler());
				threadOne.start();
				
				
				TraceUtil.trace(this, "Player 1 connected.");
				// This agent is the first good guy, change server's state to serve the second player.
				this.connectStatus = connect_one;
			} else if (this.connectStatus == connect_one){
				// if the agent is the second player
				receiver.setAgentTwo(agent);
				agentTwo = agent;
				
				// create a thread to service this player
				try {
					agent.init();
				} catch (CommunicationException e) {
					/* This is a corrupted agent, destroy it as before. Then to serve
					 * the next agent. */
					agent.destory();
					continue;
				}
				threadTwo = new Thread(agent);
				threadTwo.setUncaughtExceptionHandler(this.new AgentThreadUncaughtExceptionHandler());
				threadTwo.start();
				
				TraceUtil.trace(this, "Player 2 connected.");
				// This agent is the second good guy.
				this.connectStatus = connect_two;
			} else {
				
				// impossible path
				agent.destory();
				continue;
			}
		}
		
	} // end of run(work).
	

	/* server off line. */
	public void offline() {
		// stop the running thread so that the agent can't receive messages.
		if (threadOne != null) {
			threadOne.interrupt();
			threadOne = null;
		}
		// stop the running thread so that the agent can't receive messages.
		if (threadTwo != null) {
			threadTwo.interrupt();
			threadTwo = null;
		}
		if (agentOne != null) {
			try {
				// send message to tell the player, server runs into a accident.
				agentOne.send(ProtocolUtil.makeServerOffline() + ProtocolUtil.end);
			} catch (CommunicationException e) {
				/* close it regardless the exception */
				TraceUtil.trace(this, e.getMessage());
			} finally {
				agentOne.destory();
				agentOne = null;
			}
		}
		if (agentTwo != null) {
			try {
				// send message to tell the player, server runs into a accident.
				agentTwo.send(ProtocolUtil.makeServerOffline() + ProtocolUtil.end);
			} catch (CommunicationException e) {
				/* close it regardless the exception */
				TraceUtil.trace(this, e.getMessage());
			} finally {
				agentTwo.destory();
				agentTwo = null;
			}
		}
	}
	public void clientOneOffline() {
		// stop the running thread so that the agent can't receive messages.
		if (threadOne != null) {
			threadOne.interrupt();
			threadOne = null;
		}
		// stop the running thread so that the agent can't receive messages.
		if (threadTwo != null) {
			threadTwo.interrupt();
			threadTwo = null;
		}
		// agent two run into a accident, so it can't receive a message.
		if (agentOne != null) {
			agentOne.destory();
			agentOne = null;
		}
		if (agentTwo != null) {
			try {
				// send message to tell another player that this player runs to an accident.
				agentTwo.send(ProtocolUtil.makeAnotherOffline() + ProtocolUtil.end);
			} catch (CommunicationException e) {
				/* close it regardless the exception */
				TraceUtil.trace(this, e.getMessage());
			} finally {
				agentTwo.destory();
				agentTwo = null;
			}
		}
		
		this.connectStatus = connect_none;
		this.setState(CommunicationServerState.wait_first);
	}
	public void clientTwoOffline() {
		// stop the running thread so that the agent can't receive messages.
		if (threadOne != null) {
			threadOne.interrupt();
			threadOne = null;
		}
		// stop the running thread so that the agent can't receive messages.
		if (threadTwo != null) {
			threadTwo.interrupt();
			threadTwo = null;
		}
		if (agentOne != null) {
			try {
				// send message to tell another player that this player runs to an accident.
				agentOne.send(ProtocolUtil.makeAnotherOffline() + ProtocolUtil.end);
			} catch (CommunicationException e) {
				/* close it regardless the exception */
				TraceUtil.trace(this, e.getMessage());
			} finally {
				agentOne.destory();
				agentOne = null;
			}
		}
		// agent two run into a accident, so it can't receive a message.
		if (agentTwo != null) {
			agentTwo.destory();
			agentTwo = null;
		}
		
		this.connectStatus = connect_none;
		this.setState(CommunicationServerState.wait_first);
	}
	/* Agent thread uncaught exception handler.
	 * This class just is used to capture the thread's uncaught exception, so the code
	 * is little. Exception handling code should be written in server class.
	 */
	class AgentThreadUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
		
		/*
		 * 
		 * @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang.Thread, java.lang.Throwable)
		 */
		@Override
		public void uncaughtException(Thread thread, Throwable throwable) {
			TraceUtil.trace(this, "Agent thread runs to a uncaught exception.");
			
			// destroy the agent
			if (thread == CommunicationServer.this.threadOne) {
				CommunicationServer.this.clientOneOffline();
			} else if (thread == CommunicationServer.this.threadTwo){
				CommunicationServer.this.clientTwoOffline();
			}
		}
		
	}
}
